﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Castle.DynamicProxy;
using Moki.Exceptions;
using Moki.Extensions;

namespace Moki.Core
{
    /// <summary>
    /// Manages mock object calls. Assigns invocation values according to mocking state.
    /// </summary>
    internal class CallManager
    {
        public CallManager()
        {
            Stubs = new List<StubContext>();
            Calls = new List<CallInvocation>();
            EventSubscribersMap = new Dictionary<string, MulticastDelegate>();
        }

        public MockRepository Repository { get; set; }

        /// <summary>
        /// List of all mock object stubs. 
        /// </summary>
        public List<StubContext> Stubs { get; private set; }
        public List<CallInvocation> Calls { get; private set; }

        public CallConstraints CallConstraints { get; set; }

        public bool InterceptsPartialMock { get; set; }

        /// <summary>
        /// Map event to its subsribers.
        /// </summary>
        private IDictionary<string, MulticastDelegate> EventSubscribersMap { get; set; }

        /// <summary>
        /// Add stub to list.
        /// </summary>
        /// <param name="stub"></param>
        internal void AddStub(StubContext stub)
        {
            Stubs.Add(stub);
        }

        private void SubscribeEvent(string eventName, Delegate subscriber)
        {
            if (!EventSubscribersMap.ContainsKey(eventName))
                EventSubscribersMap[eventName] = (MulticastDelegate)subscriber;
            else
            {
                var actual = EventSubscribersMap[eventName];
                EventSubscribersMap[eventName] = (MulticastDelegate)MulticastDelegate.Combine(actual, subscriber);
            }
        }

        private void UnsubscribeEvent(string eventName, Delegate subscriber)
        {
            if (EventSubscribersMap.ContainsKey(eventName))
            {
                var actual = EventSubscribersMap[eventName];
                EventSubscribersMap[eventName] = (MulticastDelegate)MulticastDelegate.Remove(actual, subscriber);
            }
        }

        /// <summary>
        /// Tries get given method call with given arguments when this call was stubbed. 
        /// </summary>
        /// <param name="method"></param>
        /// <param name="invocationArguments"></param>
        /// <param name="call"></param>
        /// <returns>True if call was stubbed, otherwise False. </returns>
        private bool TryGetStub(MethodInfo method, Arguments invocationArguments, out StubContext call)
        {
            call = Stubs.FirstOrDefault(c => c.Method.Name == method.Name && c.Arguments.Matches(invocationArguments));
            return call != null;
        }

        /// <summary>
        /// Saves invocation to calls list used in veryfing.
        /// </summary>
        /// <param name="method"></param>
        /// <param name="arguments"></param>
        private void SaveCall(MethodInfo method, Arguments arguments)
        {
            var call = Calls.Find(c => c.Method.Name == method.Name && c.Arguments.Matches(arguments));

            if (call == null)
            {
                call = new CallInvocation(method, arguments);
                Calls.Add(call);
            }

            call.IncrementTimes();
        }

        /// <summary>
        /// Creates stub for property given by set method
        /// </summary>
        /// <param name="proxy">Proxy object</param>
        /// <param name="setMethod">Set method</param>
        /// <param name="returnValue">Property Set value</param>
        private void CreateGetStub(object proxy, MethodInfo setMethod, object returnValue)
        {
            MethodInfo getMethod = setMethod.ToProperty(proxy.GetType()).GetGetMethod();

            StubContext getStub = Stubs.FirstOrDefault(c => c.Method.Name == getMethod.Name);

            if (getStub == null)
            {
                getStub = new StubContext(getMethod, new Arguments());
                AddStub(getStub);
            }

            if (getStub.HasResult)
                getStub.CurrentResult.ReturnValue = returnValue;
            else
                getStub.AddResultValue(returnValue);
        }

        /// <summary>
        /// Resolve mocking logic for invocation in acting state. 
        /// </summary>
        /// <param name="invocation"></param>
        public void ResolveCall(IInvocation invocation)
        {
            var arguments = new Arguments(invocation.Arguments);
            var method = invocation.Method;
            var proxy = invocation.Proxy;
            StubContext stub;

            if (method.IsEventAdd())
            {
                SubscribeEvent(method.GetEventName(), (Delegate)arguments[0]);
            }
            else if (method.IsEventRemove())
            {
                UnsubscribeEvent(method.GetEventName(), (Delegate)arguments[0]);
              
            }
            else if (TryGetStub(method, arguments, out stub))   // when stub for this invocation exists
            {
                stub.Invoke();
                invocation.ReturnValue = stub.LastReturnValue;
            }
            else if (CallConstraints == CallConstraints.Strict)
            {
                // when method this call was not stubbed 
                throw new CallConstraintsException(String.Format(
                    "Call member {0} is not allowed because was not stubbed and mock {1} have strict constraints",
                    method.Name,
                    proxy.ToString()));
            }
            else
            {
                stub = new StubContext(method, arguments);

                if (method.IsPropertySet() && !stub.IgnoreSet)
                {
                    CreateGetStub(proxy, method, arguments[0]);
                }

                if (InterceptsPartialMock)
                {
                    invocation.Proceed();
                    stub.AddResultValue(invocation.ReturnValue);
                }
                else
                {
                    stub.AddResultDefault(invocation.Method.ReturnType);
                    invocation.ReturnValue = stub.CurrentResult.ReturnValue;
                }

                stub.Invoke();
                AddStub(stub);
            }

            SaveCall(method, arguments);
        }

        /// <summary>
        /// Resolve mocking logic for invocation in stubbing state. 
        /// </summary>
        /// <param name="invocation"></param>
        internal void ResolveStub(IInvocation invocation)
        {
            var arguments = new Arguments(invocation.Arguments);
            var method = invocation.Method;
            var proxy = invocation.Proxy;
            StubContext stub;

            if (method.IsEvent())
            {
                StubContext lastStub = Stubs.LastOrDefault();
                lastStub.AddEventToResult(EventSubscribersMap[method.GetEventName()]);
            }
            else if (TryGetStub(method, arguments, out stub))
            {   // assign to return value existing recursive mock
                invocation.ReturnValue = stub.CurrentResult.ReturnValue;
            }
            else
            {
                stub = new StubContext(method, arguments);

                TryCreateRecursiveMockWhenRequired(stub);

                // assign to return value resursive mock
                invocation.ReturnValue = stub.CurrentResult.ReturnValue;

                AddStub(stub);
            }
        }

        /// <summary>
        /// Resolve mocking logic for invocation in verifying state. 
        /// </summary>
        /// <param name="invocation"></param>
        internal void ResolveVerify(IInvocation invocation)
        {
            var arguments = new Arguments(invocation.Arguments);
            var method = invocation.Method;
            var proxy = invocation.Proxy;
            StubContext stub;

            if (TryGetStub(method, arguments, out stub))    // assign to return value resursive mock
                invocation.ReturnValue = stub.CurrentResult.ReturnValue;
            else
                throw new VerifyException(String.Format("Method {0} has been never called or stubbed", method.Name));
        }

        internal Delegate GetEventToRaise(IInvocation invocation)
        {
            var method = invocation.Method;

            if (method.IsEvent())
            {
                string eventName = method.GetEventName();

                if (EventSubscribersMap.ContainsKey(eventName))
                    return EventSubscribersMap[eventName];
                else
                    throw new RaiseEventException(String.Format("Event {0} does not exists", eventName));
            }
            else
                throw new RaiseEventException("Method is not a event");
        }

        #region Recursive Mocks
        /// <summary>
        /// Determines whether given type requires automatic mock
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private bool TypeNeedMock(Type type)
        {
            if (type.IsSealed)
                throw new MockTypeException(String.Format("Can't create automatic mock for sealed class {0}", type.Name));

            return type.IsInterface || type.IsClass;
        }

        /// <summary>
        /// Tries create recursive (automatic) mock for given call.
        /// </summary>
        /// <param name="proxyType"></param>
        /// <param name="call"></param>
        /// <returns></returns>
        private bool TryCreateRecursiveMockWhenRequired(StubContext call)
        {
            if (TypeNeedMock(call.Method.ReturnType))
            {
                call.AddResultValue(CreateMockFor(call.Method));
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Creates mock of method return type.
        /// </summary>
        /// <param name="method"></param>
        /// <returns></returns>
        private object CreateMockFor(MethodInfo method)
        {
            return Repository.CreateMock(method.ReturnType, CallConstraints);
        }
        #endregion
    }
}
